/*
 * Copyright 2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.gitee.joyz.api.result.i18n;

import io.gitee.joyz.api.result.logging.Log;
import io.gitee.joyz.api.result.logging.LogFactory;
import io.gitee.joyz.api.result.util.ClassUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 基于 {@link ResourceBundle} 类实现的国际化消息类
 * 该类可以加载 properties 资源包, 并通过消息代码, 语言环境解析资源包
 * 中配置的国际化消息, 同时该类还会对读取的资源包进行缓存
 *
 * @author JoyZ
 */
public class ResourceBundleLocaleMessage extends AbstractResourceLocaleMessage {


    private final Log log = LogFactory.getLogger(getClass());

    private final ResourceBundleControl control = new ResourceBundleControl();

    private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles = new ConcurrentHashMap<>(2);

    private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedMessageFormats = new ConcurrentHashMap<>();

    /**
     * 构造器
     */
    public ResourceBundleLocaleMessage() {
        setDefaultEncoding("ISO-8859-1");
    }

    @Override
    protected MessageFormat resolveMessage(String code, Locale locale) {
        Set<String> baseNames = getBaseNames();

        for (String baseName : baseNames) {
            ResourceBundle bundle = getResourceBundle(baseName, locale);
            if (bundle != null) {
                MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
                if (messageFormat != null) {
                    return messageFormat;
                }
            }
        }
        return null;
    }

    /**
     * 从缓存中获取 {@link MessageFormat} 实例
     *
     * @param bundle 资源包实例
     * @param code   消息代码
     * @param locale 语言环境
     * @return 返回一个从缓存中获取的 {@link MessageFormat} 实例
     * @throws MissingResourceException 如果未找到资源包, 则抛出异常
     */
    protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) throws MissingResourceException {
        Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedMessageFormats.get(bundle);
        Map<Locale, MessageFormat> localeMap = null;
        if (codeMap != null) {
            localeMap = codeMap.get(code);
            if (localeMap != null) {
                MessageFormat result = localeMap.get(locale);
                if (result != null) {
                    return result;
                }
            }
        }

        String message = findByCode(bundle, code);
        if (message != null) {
            if (codeMap == null) {
                codeMap = this.cachedMessageFormats.computeIfAbsent(bundle, b -> new ConcurrentHashMap<>());
            }
            if (localeMap == null) {
                localeMap = codeMap.computeIfAbsent(code, c -> new ConcurrentHashMap<>());
            }
            MessageFormat result = createMessageFormat(message, locale);
            localeMap.put(locale, result);
            return result;
        }

        return null;
    }

    /**
     * 尝试在给定的 bundle 中查找给定 key 的消息
     *
     * @param bundle 资源包实例
     * @param key    资源项的 key
     * @return 返回一个查找到的资源项, 如果未找到则返回 null
     */
    protected String findByCode(ResourceBundle bundle, String key) {
        if (bundle.containsKey(key)) {
            try {
                return bundle.getString(key);
            } catch (MissingResourceException ex) {
                // 不抛出异常, 继续从父消息组件进行查找
            }
        }
        return null;
    }

    /**
     * 从缓存中获取资源包.
     *
     * @param baseName 资源包名称
     * @param locale   语言环境
     * @return 返回一个资源包实例
     * @throws MissingResourceException 如果未找到资源包则抛出该异常
     */
    protected ResourceBundle getResourceBundle(String baseName, Locale locale) {
        if (getCacheTime() >= 0) {
            return getBundle(baseName, locale);
        } else {
            Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(baseName);
            if (localeMap != null) {
                ResourceBundle bundle = localeMap.get(locale);
                if (bundle != null) {
                    return bundle;
                }
            }
            try {
                ResourceBundle bundle = getBundle(baseName, locale);
                if (localeMap == null) {
                    localeMap = this.cachedResourceBundles.computeIfAbsent(baseName, bn -> new ConcurrentHashMap<>());
                }
                localeMap.put(locale, bundle);
                return bundle;
            } catch (MissingResourceException ex) {
                log.error(ex.getMessage());
                return null;
            }
        }
    }

    /**
     * 根据给定 baseName, locale 从资源文件获取资源实例
     *
     * @param baseName 资源包名称
     * @param locale   语言环境
     * @return 返回获取的资源实例
     */
    protected ResourceBundle getBundle(String baseName, Locale locale) {

        ClassLoader classLoader = getClassLoader();

        if (classLoader == null) {
            classLoader = ClassUtils.getDefaultClassLoader();
        }

        assert (classLoader != null) : "No bundle ClassLoader set";

        if (locale == null) {
            locale = getDefaultLocale();
        }

        try {
            return ResourceBundle.getBundle(baseName, locale, classLoader, control);
        } catch (UnsupportedOperationException e) {
            String encoding = getDefaultEncoding();
            if (encoding != null) {
                log.info("ResourceBundleMessageSource is configured to read resources with encoding '" + encoding + "' but ResourceBundle.Control not supported in current system environment: " + e.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " + "platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " + "for participating in the platform default and therefore avoiding this log message.");
            }
        }

        return ResourceBundle.getBundle(baseName, locale, classLoader);
    }


    /**
     * 从给定的 Reader 加载基于属性的资源包.
     * <p>只有当使用 {@link #setDefaultEncoding(String) setDefaultEncoding} 方法设置了
     * 默认资源文件编码时该方法才会被调用
     *
     * @param reader 指定 reader
     * @return 已加载的资源包
     * @throws IOException 当发生IO错误时, 抛出该异常
     */
    protected ResourceBundle loadBundle(Reader reader) throws IOException {
        return new PropertyResourceBundle(reader);
    }

    /**
     * 从给定的 InputStream 加载基于属性的资源包.
     * <p>仅当用未设置默认资源文件编码时, 才会调用该方法
     *
     * @param inputStream 指定的输入流
     * @return 已加载的资源包
     * @throws IOException 发生IO错误时, 抛出该异常
     */
    protected ResourceBundle loadBundle(InputStream inputStream) throws IOException {
        return new PropertyResourceBundle(inputStream);
    }

    /**
     * 自定义 {@link ResourceBundle.Control} 类实现.
     * 支持根据文件编码读取获取资源文件, 语言环境回退, 缓存等功能
     */
    private class ResourceBundleControl extends ResourceBundle.Control {

        private static final String SUFFIX_NAME = "properties";

        private static final String FORMAT = "java.properties";


        @Override
        public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader classLoader, boolean reload) throws IllegalAccessException, InstantiationException, IOException {

            if (format.equals(FORMAT)) {
                String bundleName = toBundleName(baseName, locale);
                String resourceName = toResourceName(bundleName, SUFFIX_NAME);

                InputStream inputStream = null;

                if (!reload) {
                    URL url = classLoader.getResource(resourceName);
                    if (url != null) {
                        URLConnection connection = url.openConnection();
                        if (connection != null) {
                            connection.setUseCaches(false);
                            inputStream = connection.getInputStream();
                        }
                    }
                } else {
                    inputStream = classLoader.getResourceAsStream(resourceName);
                }

                if (inputStream == null) {
                    return null;
                }

                String encoding = getDefaultEncoding();

                if (encoding != null) {
                    try (InputStreamReader reader = new InputStreamReader(inputStream, encoding)) {
                        return loadBundle(reader);
                    }
                } else {
                    try (InputStream inputs = inputStream) {
                        return loadBundle(inputs);
                    }
                }
            }
            return super.newBundle(baseName, locale, format, classLoader, reload);
        }

        @Override
        public Locale getFallbackLocale(String baseName, Locale locale) {
            Locale defaultLocale = getDefaultLocale();
            return (defaultLocale != null && !defaultLocale.equals(locale)) ? defaultLocale : null;
        }

        @Override
        public long getTimeToLive(String baseName, Locale locale) {
            long cacheTime = getCacheTime() * 1000L;
            return (cacheTime >= 0) ? cacheTime : super.getTimeToLive(baseName, locale);
        }
    }
}
